import Logger from "./utils/Logger"
import MessageBus, { messageType } from "./message/MessageBus"

import RenderLayers from "./utils/RenderLayers"
import { AppPhaseChangeMessage } from "./message/app.message"
import Profiler from "./Profiler"
import CommandBinder from "./CommandBinder"
import Market from "./Market"
import ModuleLoader from "./ModuleLoader"
import EngineContext from "./EngineContext"
import OpenDeferred from "./utils/OpenDeferred"
import SymbolLoader from "./SymbolLoader"
import EngineGenerators from "./engine/EngineGenerators"
import { GeneratorsProcess } from "./enum/engine.enum"
import { phaseState } from "./enum/phase.enum"
import funcUtil from "./utils/func.util"
import Showcase from "./Showcase"
import { XRFrame } from "three"
import Module from "./module/Module"
import Message from "./message/message"
let initState = phaseState.End
const f = new Logger("engine")

export default class Engine {
  moduleLookup: Record<string, { module: Module; initialized: boolean; symbol?: symbol }>
  componentLookup: Record<string, { component: any; active: boolean }[]>
  deactivatedHack: boolean
  currentApplication: Showcase
  moduleProfiler: Profiler
  commandBinder: CommandBinder
  market: Market
  messageBus: MessageBus
  engineGenerators: EngineGenerators
  layers: RenderLayers
  moduleLoader: ModuleLoader
  symbolLoader: SymbolLoader
  afterPhasePromises: OpenDeferred[]
  afterPhaseListenerCount: number[]
  moduleLoadInjectionHook: Function
  lastTick: number
  constructor() {
    this.moduleLookup = {}
    this.componentLookup = {}
    this.deactivatedHack = !1
    this.currentApplication = null
    this.moduleProfiler = new Profiler()
    this.commandBinder = new CommandBinder()
    this.market = new Market()
    this.messageBus = new MessageBus()
    this.engineGenerators = new EngineGenerators()
    this.layers = new RenderLayers()
    this.moduleLoader = new ModuleLoader()
    this.symbolLoader = new SymbolLoader()
    this.afterPhasePromises = []
    this.afterPhaseListenerCount = []
    this.resetPromises()
    this.subscribe(AppPhaseChangeMessage, e => this.moduleProfiler.addAppPhase(e.phase))
  }
  async loadApplication(e: Showcase, t?: Function, autoPlay?: boolean) {
    if (e === this.currentApplication) throw Error("Application already loaded")
    const n = (this.currentApplication ? this.currentApplication.getManifest() : []).map(e => e.type),
      s = e.getManifest(),
      r = s.map(e => e.type),
      o = this.currentApplication
    this.currentApplication = null
    const a = n.filter(e => -1 === r.indexOf(e)),
      l = a.filter(e => "symbol" != typeof e),
      c = a.filter(e => "symbol" == typeof e),
      h = [...l.map(e => this.unloadModule(e)), ...c.map(e => this.unloadModuleBySymbol(e))]
    o && h.push(o.unload(this))
    const d = s.filter(e => -1 === n.indexOf(e.type))
    for (const e of d)
      "symbol" == typeof e.type
        ? this.symbolLoader.moduleRegistered(e.type) || this.registerModule(e)
        : this.moduleLoader.moduleRegistered(e.type) || this.registerModule(e)
    if (h.length)
      try {
        await Promise.all(h)
      } catch (e) {
        f.error(e), t && t(e)
      }
    try {
      this.currentApplication = e
      await e.load(this, { coldStart: null === o, autoPlay })
    } catch (e) {
      f.error(e), t && t(e)
    }
  }
  registerModule(e: { type: symbol | typeof Module; promise: () => Promise<typeof Module> }) {
    if (this.currentApplication) throw Error("Cannot register modules while application is running")
    "symbol" == typeof e.type ? this.symbolLoader.registerModule(e.type, e.promise) : this.moduleLoader.registerModule(e.type, e.promise)
  }
  loadModule<A extends typeof Module = typeof Module>(config: { type: A; config?: any }) {
    const t = config
    this.moduleLoadInjectionHook && (t.config = this.moduleLoadInjectionHook(t.type, t.config || {}))
    const i = new EngineContext(this)
    const n = Date.now()
    const s = this.moduleLoader.loadModule(i, t.type, t.config)
    s.ctorPromise.then(
      e =>
        (this.moduleLookup[this.moduleLoader.getTypeID(t.type)] = {
          module: e,
          initialized: !1
        })
    )
    s.initPromise.then(e => {
      this.moduleProfiler.addModuleLoadTime(e.name, n, Date.now(), i.getLoadedDependencies())
      this.moduleLookup[this.moduleLoader.getTypeID(t.type)].initialized = !0
    })
    return s.initPromise.nativePromise()
  }
  loadModuleBySymbol<A extends typeof Module = typeof Module>(config: { type: symbol; config?: any }) {
    const i = String(config.type)
    this.moduleLoadInjectionHook && (config.config = this.moduleLoadInjectionHook(i, config.config || {}))
    const n = new EngineContext(this)
    const s = Date.now()
    const r = this.symbolLoader.loadModule<A>(n, config.type, config.config)
    r.ctorPromise.then(
      e =>
        (this.moduleLookup[i] = {
          symbol: config.type,
          module: e,
          initialized: !1
        })
    )
    r.initPromise.then(e => {
      this.moduleProfiler.addModuleLoadTime(e.name, s, Date.now(), n.getLoadedDependencies())
      this.moduleLookup[i].initialized = !0
    })
    return r.initPromise.nativePromise()
  }
  setLoadInjection(e) {
    if (void 0 !== this.moduleLoadInjectionHook) throw Error("Hook already registered")
    this.moduleLoadInjectionHook = e
  }
  async unloadModule(t) {
    if (!this.moduleLoader.moduleLoaded(t)) return void f.debug(`${funcUtil.getClassName(t)} module already unloaded`)
    f.debug(`Unloading ${funcUtil.getClassName(t)} module`)
    const i = await this.getModule(t)
    const n = this.getModuleIndex(i)
    if (this.componentLookup[n]) {
      await Promise.all(
        this.componentLookup[n].map(async e => {
          await this.removeComponent(i, e.component), e.component.dispose()
        })
      )
      this.componentLookup[n] = []
    }
    this.moduleLoader.unloadModule(t)
    i.dispose(this)
  }
  async unloadModuleBySymbol(e) {
    if (!this.symbolLoader.moduleLoaded(e)) return void f.debug(`${String(e)} module already unloaded`)
    f.debug(`Unloading ${String(e)} module`)
    const t = await this.getModuleBySymbol(e)
    const i = this.getModuleIndex(t)
    if (this.componentLookup[i]) {
      await Promise.all(
        this.componentLookup[i].map(async e => {
          await this.removeComponent(t, e.component), e.component.dispose()
        })
      )
      this.componentLookup[i] = []
    }
    this.symbolLoader.unloadModule(e)
    t.dispose(this)
  }
  async waitForLoadingModules() {
    try {
      await Promise.all([this.moduleLoader.whenQueueEmpties(), this.symbolLoader.whenQueueEmpties()])
    } catch (e) {
      console.log(e)
    }
  }
  async getModule<T extends typeof Module = typeof Module>(e: T) {
    return this.moduleLoader.getModule<T>(e)
  }
  getModuleBySymbol<T extends Module = Module>(e: symbol) {
    return this.symbolLoader.getModule<T>(e)
  }
  getModuleSync<T extends typeof Module = typeof Module>(e: T) {
    return this.moduleLoader.getModuleSync(e)
  }
  getComponentByType(e) {
    for (const t in this.moduleLookup) {
      const i = this.moduleLookup[t].module
      const n = this.getModuleIndex(i)
      const s = this.componentLookup[n] || []
      for (const t of s) if (t.component instanceof e) return t.component
    }
    return null
  }
  async addComponent<T extends Module = Module>(e: T, component: any) {
    const moduleName = this.getModuleIndex(e)
    let components = this.componentLookup[moduleName] || []
    this.componentLookup[moduleName] = components
    let index = -1
    for (let e = 0; e < components.length; e++)
      if (components[e].component === component) {
        index = e
        break
      }
    if (index === -1) {
      index = components.push({ component, active: !1 }) - 1
      //Component 初始化
      component.init()
    }
    if (components[index].active) throw Error("Tried to add already active component: " + typeof component)
    //Component 激活
    await component.activate(this)
    components[index].active = !0
  }
  async removeComponent<T extends Module = Module>(e: T, t) {
    const i = this.getModuleIndex(e)
    const n = this.componentLookup[i]
    if (n) {
      let e = null
      for (const i of n)
        if (i.component === t) {
          e = i
          break
        }
      e && e.active && (await t.deactivate(this), (e.active = !1))
    }
  }
  tick() {
    if (this.deactivatedHack) return
    if (initState !== phaseState.End) throw Error("Engine.tick called outside of waiting. Did the engine get called recursively?!")
    initState = phaseState.Begin
    this.resolveAfterPhase(phaseState.Begin)
    const e = performance.now()
    const t = this.lastTick ? e - this.lastTick : 16
    this.lastTick = e
    initState = phaseState.Logic
    try {
      for (const e in this.moduleLookup) {
        const i = this.moduleLookup[e]
        i && i.initialized && i.module.onUpdate(t)
      }
    } catch (e) {
      throw (f.error(e), e)
    }
    this.resolveAfterPhase(phaseState.Logic)
    this.engineGenerators.processGenerators(GeneratorsProcess.Phase, phaseState.Logic)
    try {
      this.renderComponents(t)
    } catch (e) {
      throw (f.error(e), e)
    }
    try {
      initState = phaseState.Render
      this.resolveAfterPhase(phaseState.Render)
      this.engineGenerators.processGenerators(GeneratorsProcess.Phase, phaseState.Render)
      initState = phaseState.End
      this.resolveAfterPhase(phaseState.End)
      this.engineGenerators.processGenerators(GeneratorsProcess.None)
      this.engineGenerators.processGenerators(GeneratorsProcess.Promise)
      this.engineGenerators.processGenerators(GeneratorsProcess.Duration)
    } catch (e) {
      throw (f.error(e), e)
    }
  }
  subscribe<A extends typeof Message = typeof Message>(e: A, t: (e?: A["prototype"]) => any, i = messageType.PERMANENT) {
    return this.messageBus.subscribe(e, t, i)
  }
  unsubscribe(e, t) {
    this.messageBus.unsubscribe(e, t)
  }
  broadcast<A extends Message>(e: A) {
    this.messageBus.broadcast(e)
  }
  claimRenderLayer(e) {
    return this.layers.claimLayer(e)
  }
  getRenderLayer(e) {
    return this.layers.getLayer(e)
  }
  disposeRenderLayer(e) {
    this.layers.disposeLayer(e)
  }
  startGenerator(e: () => Generator) {
    this.engineGenerators.startGenerator(e)
  }
  stopGenerator(e: () => Generator) {
    this.engineGenerators.stopGenerator(e)
  }
  after(e: number) {
    this.afterPhaseListenerCount[e]++
    return this.afterPhasePromises[e].nativePromise()
  }
  getModuleIndex(e: Module) {
    for (const t in this.moduleLookup) if (this.moduleLookup[t] && this.moduleLookup[t].module === e) return t
    return ""
  }
  *getComponents() {
    for (const e in this.moduleLookup) {
      const t = this.componentLookup[e]
      if (t) for (const e of t) yield e
    }
  }
  toggleRendering<T extends Module = Module>(e: T, t: boolean) {
    const i = this.getModuleIndex(e)
    const n = this.componentLookup[i]
    if (n) for (const e of n) e.active = t
  }
  renderComponents(e: number) {
    for (const t in this.moduleLookup) {
      const i = this.componentLookup[t]
      if (i) for (const t of i) t.active && t.component.beforeRender && t.component.beforeRender(e)
    }
    for (const t in this.moduleLookup) {
      if (!t) continue
      const i = this.componentLookup[t]
      if (i) for (const t of i) t.active && t.component.render(e)
    }
  }
  resetPromises() {
    this.resetAfterPhasePromises()
  }
  resetAfterPhasePromises() {
    this.resetAfterPhasePromise(phaseState.Logic)
    this.resetAfterPhasePromise(phaseState.Render)
    this.resetAfterPhasePromise(phaseState.Begin)
    this.resetAfterPhasePromise(phaseState.End)
  }
  resetAfterPhasePromise(e) {
    this.afterPhasePromises[e] = new OpenDeferred()
    this.afterPhaseListenerCount[e] = 0
  }
  resolveAfterPhase(e) {
    this.afterPhaseListenerCount[e] > 0 && (this.afterPhasePromises[e].resolve(), this.resetAfterPhasePromise(e))
  }
  deactivate() {
    for (const e in this.moduleLookup) {
      const t = this.moduleLookup[e]
      if (t && t.initialized)
        if (t.symbol) this.unloadModuleBySymbol(t.symbol)
        else {
          const e = this.getModuleType(t.module)
          e && this.unloadModule(e)
        }
    }
    this.deactivatedHack = !0
  }
  xrFrame: XRFrame
  setXrFrame(e: XRFrame) {
    this.xrFrame = e
  }
  getXrFrame() {
    return this.xrFrame
  }
  getModuleType<T extends Module = Module>(e: T) {
    for (const t in this.moduleLookup) {
      const i = this.moduleLookup[t]
      if (i && i.module === e) return this.moduleLoader.getTypeFromTypeID(t)
    }
    return null
  }
  get msgBus() {
    return this.messageBus
  }
}
